/**
 * @file
 * Main application implementation.
 *
 * Kisa provides spell checking as you type and displays the result in a small
 * pop-up window on your X11 desktop or from the system tray.
 *
 * Copyright (c) 2009 by Pete Black <theblackpeter@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * @author Pete Black <theblackpeter@gmail.com>
 */
#include <cstdlib>

#include <QtGui>
#include <QtDebug>
#include <QMenu>
#include <QProcess>
#include <QScrollBar>

#include "kisa.h"
#include "kisawidget.h"
#include "kisasettings.h"
#include "kisathread.h"

/**
 * Clear text after being idle for this many seconds.
 * Default value is 20 s.
 */
#ifndef IDLE_CLEAR_TEXT_TIME
#define IDLE_CLEAR_TEXT_TIME 20000
#endif

/**
 * Name used when outputting debug information.
 */
#ifndef OBJECT_DEBUG_NAME
#define OBJECT_DEBUG_NAME "kisa:"
#endif

/**
 * Instances of this class will have this name.
 * @note this is not a unique identifier, just a common name.
 */
#ifndef OBJECT_NAME
#define OBJECT_NAME "kisa"
#endif

/**
 * The project name.
 */
#ifndef PROJECT_NAME
#define PROJECT_NAME "Kisa"
#endif

/**
 * The current project version.
 */
#ifndef PROJECT_VERSION
#define PROJECT_VERSION "0.63"
#endif

/**
 * Default help manual URL.
 */
#ifndef DEFAULT_MANUAL_URL
#define DEFAULT_MANUAL_URL "http://kisaspellchecker.googlepages.com/manual"
#endif

Kisa::Kisa(int& argc, char** argv) :
  QApplication(argc, argv) {

  // check for system tray icon availability
  if(!QSystemTrayIcon::isSystemTrayAvailable())
    qFatal("No system tray available.");

  // the translator needs to be created before any of the widgets
  kisaTranslator = new QTranslator(this);

  // load a default translator based on current system locale
  // we'll load the user requested in init based on the -language flag
  loadTranslations();

  // the corresponding command line arguments will be stored here
  QString display;
  QString language;
  QString iconsPath;
  QString flagsPath;
  QString dictDir;
  QString aspellPrefix;
  QString extraDicts;

  parseInputArguments(argc, argv, display, language, iconsPath, flagsPath,
                      dictDir, aspellPrefix, extraDicts);

  qDebug() << OBJECT_NAME << "starting" << PROJECT_NAME << "version" << PROJECT_VERSION;

  init(display, language, iconsPath, flagsPath, dictDir, aspellPrefix,
       extraDicts);

  // start worker thread
  startThread();

  // make sure this is the last thing we do
  if(kisaSettings->closedVisible()) {
    qDebug() << OBJECT_DEBUG_NAME << "was closed visible, bringing up main widget";
    kisaWidget->show();
  }
  else {
    qDebug() << OBJECT_DEBUG_NAME << "was closed hidden, starting main widget hidden";
    kisaWidget->hide();
    stopThread();
  }

  kisaTrayIcon->show();

  qDebug() << OBJECT_DEBUG_NAME << OBJECT_NAME << "created";
  qDebug() << PROJECT_NAME << "version" << PROJECT_VERSION << "started";
}

Kisa::~Kisa() {
  kisaSettings->writeSettings();

  delete kisaTrayIcon; // not needed, owned by kisaWidget
  delete completer; // not needed, owned by kisaWidget (via lineEdit)
  delete kisaSettings; // not needed owned by kisaWidget
  delete kisaThread; // thread knows how to stop itself, owned by this
  delete kisaTimer; // not needed, owned by this
  delete kisaTranslator; // not needed, owned by this
  delete kisaWidget;
  delete kisaLib;

  qDebug() << OBJECT_DEBUG_NAME << OBJECT_NAME << "destroyed";
  qDebug() << OBJECT_DEBUG_NAME << "exiting program";
}

void Kisa::aboutQt() {
  QMessageBox::aboutQt(kisaWidget);
}

void Kisa::addCharacter(const QChar& key) {
  // ignore empty characters such as keyboard modifiers
  if(key.isNull()) {
    qDebug() << OBJECT_DEBUG_NAME << "ignoring keyboard modifier";
    return;
  }

  //ignore if first character is a none letter character
  if(wordBuffer.isEmpty() && !key.isLetter()) {
    qDebug() << OBJECT_DEBUG_NAME << "ignoring first non-letter";
    return;
  }

  // cheap hack to overcome getting keyboard layout bug
  // TODO: remove this when keyboard layout events start working again
  if((wordBuffer.isEmpty() || wordIsChecked) && key.isLetter() &&
      kisaSettings->updateOnNewLayout()) {
    qDebug() << OBJECT_DEBUG_NAME << "checking for new keyboard layout";
    updateOnNewKeyboardLayout();
  }

  // if the word is checked already, clear it and start over depending on
  // what the next key is
  if(wordIsChecked) {
    // clear the word only if we get a new letter as otherwise a correction
    // on the word could still be made
    if(key.isLetterOrNumber()) {
      wordIsChecked = false;
      wordBuffer.clear();
      kisaWidget->lineEdit->clear();
    }
    // backspace only if last character was valid
    else if(key == '\b') {
      wordIsChecked = false;
    }
    // store everything else and ignore it
    else {
      wordBuffer += key;
      return;
    }

    // reset the syle sheet and the font
    kisaWidget->lineEdit->setStyleSheet("");
    kisaWidget->lineEdit->setFont(kisaSettings->font());

    // change the mouse pointer to default
    kisaWidget->lineEdit->setCursor(Qt::ArrowCursor);

    completer->popup()->hide();
  }

  // OK, so the word has not been checked yet, build it...
  // backspace
  if(!wordBuffer.isEmpty() && key == '\b') {
    QString word = kisaWidget->lineEdit->text();
    // updated the displayed text if its at the same level as the buffer
    if(word == wordBuffer) {
      wordBuffer.chop(1);
      kisaWidget->lineEdit->setText(wordBuffer);
    }
    else {
      wordBuffer.chop(1);
    }
  }
  // white space and punctuation will trigger a spell check on the word
  else if(isValidPunctuation(key)) {
    QString word = kisaWidget->lineEdit->text();
    qDebug() << OBJECT_DEBUG_NAME << "spell check on" << word << "check triggered by punctuation"  << key;
    checkWord(word);

    wordIsChecked = true;
    wordBuffer += key;
  }
  // just add everything else to the buffer
  else {
    wordBuffer += key;
  }

  if(isValidForOutput(key) && isValidForOutput(wordBuffer)) {
    kisaWidget->lineEdit->setText(wordBuffer);
  }
  
  // start the timer to clear any input if idle for more then this many seconds
  kisaTimer->start(IDLE_CLEAR_TEXT_TIME);
}

void Kisa::addDictionaryToMenu(const int& dictionaryIndex, const QIcon& icon) {
  // name of the dictionary
  QString dictionary = kisaLib->dictionaries().at(dictionaryIndex);

  // check if the dictionary is already in the menu
  if(!dictionarySeenBefore(dictionaryIndex)) {
    // have to do it this way as we need to store the dictionary index value
    QAction* dictionaryAction = new QAction(icon, dictionary, kisaToolButtonMenu);
    dictionaryAction->setData(dictionaryIndex);

    // insert action at the top
    kisaToolButtonMenu->insertAction(kisaToolButtonMenu->actions().first(),
                                 dictionaryAction);

    qDebug() << OBJECT_DEBUG_NAME << "added new dictionary" << dictionary << "to pop-up menu";
  }
  else
    qDebug() << OBJECT_DEBUG_NAME << "dictionary" << dictionary << "is already in menu pop-up, not adding";
}

void Kisa::addToPersonalWordList() {
  QString word = kisaWidget->lineEdit->text();

  if(!word.isEmpty())
    kisaLib->addToPersonalWordList(word);
}

void Kisa::checkWord(const QString& word) {
  if(kisaLib->isMisspelled(word)) {
    kisaWidget->lineEdit->setStyleSheet("QLineEdit { color: red }");
    kisaWidget->lineEdit->setFont(kisaSettings->font());

    // change the mouse pointer so it looks clickable
    kisaWidget->lineEdit->setCursor(Qt::PointingHandCursor);

    QStringList suggestions = kisaLib->getSuggestions(word);

    // the auto completer used as a pop-up to show the possible candidates
    if(kisaSettings->showSuggestionsPopup()) {
      completer->disconnect();
      delete completer;

      completer = new QCompleter(suggestions, kisaWidget->lineEdit);
      completer->setCompletionMode(QCompleter::PopupCompletion);
      completer->setCaseSensitivity(Qt::CaseInsensitive);

      QAbstractItemView* popup = completer->popup();
      popup->setFont(kisaWidget->lineEdit->font());

      kisaWidget->lineEdit->setCompleter(completer);

      connect(completer, SIGNAL(activated(const QString&)), this, SLOT(updateText(const QString&)));

      // only show pop-up if main window is actually visible
      if(!kisaWidget->isPartiallyObscured() && kisaWidget->isVisible()
          && !kisaWidget->isMinimized()) {
        // force the pop-up twice, this is needed in order to steal back focus
        completer->complete();
        popup->hide();
        completer->complete();
      }

      // we need the duration in ms
      int popupDuration = kisaSettings->suggestionsPopupDuration() * 1000;

      // we'll use a timer to close the pop-up after a given time
      // only set it if the pop-up duration is > 0
      if(popupDuration)
        QTimer::singleShot(popupDuration, popup, SLOT(hide()));
    }

    // the system tray icon pop-up
    if(kisaSettings->showSystrayPopup()) {
      QString title = tr("Kisa says:");

      QString suggestion = suggestions.first();
      QString message =
          tr("\"%1\" is unknown, did you mean \"%2\"?").arg(word, suggestion);

      // we need the duration in ms
      int popupDuration = kisaSettings->systrayPopupDuration() * 1000;

      kisaTrayIcon->showMessage(title, message, QSystemTrayIcon::Information,
                                popupDuration);
    }
  }
}

void Kisa::clear() {
  wordBuffer.clear();
  kisaWidget->lineEdit->clear();

  // change the mouse pointer to default
  kisaWidget->lineEdit->setCursor(Qt::ArrowCursor);
}

void Kisa::commitData(QSessionManager& manager) {
  const QString sessionID = manager.sessionId();

  // get the default settings file name
  QString fileName = QSettings().fileName().toLower();

  // add the session id
  fileName.replace(QRegExp("\\.conf$"), "_" + sessionID + ".conf");

  qDebug() << OBJECT_DEBUG_NAME << "saving session information to" << fileName;

  // write the settings information from the settings window
  kisaSettings->writeSettings(fileName);

  // store the used dictionaries in reverse order as we need FIFO order later
  QStringList dictionaryIndexes;
  foreach(QAction* action, kisaToolButtonMenu->actions())
      if(!action->data().isNull())
        dictionaryIndexes.prepend(action->data().toString());

  // and some extra stuff from the main window widget
  QSettings settings(fileName, QSettings::IniFormat, this);
  settings.beginGroup("Session");
  settings.setValue("dictionaryIndexes", dictionaryIndexes);
  settings.endGroup();

  // set this just in case
  manager.setRestartHint(QSessionManager::RestartIfRunning);
}

void Kisa::contextMenuEvent(const QPoint& point) {
  // get the original menu
  QMenu* menu = kisaWidget->lineEdit->createStandardContextMenu();

  QString word = "\"";
  word += kisaWidget->lineEdit->text();
  word += "\"";

  QString message = tr("Add %1 to personal word list").arg(word);

  // add to personal dictionary option
  menu->addSeparator();
  QIcon icon;
  if(QFile("icons:item_add.png").exists())
    icon = QIcon("icons:item_add.png");
  else
    icon = QIcon("icons:list-add.png");
  QAction* actionAdd = menu->addAction(icon, message, this, SLOT(addToPersonalWordList()));

  // ignore option (add to session list)
  message = tr("Ignore %1").arg(word);

  if(QFile("icons:edittrash.png").exists())
    icon = QIcon("icons:edittrash.png");
  else if(QFile("icons:trash-empty.png").exists())
    icon = QIcon("icons:trash-empty.png");
  else
    icon = QIcon("icons:list-remove.png");
  QAction* actionIgnore = menu->addAction(icon, message, this, SLOT(ignoreWord()));

  // only make the add/ignore word actio available if there is a word
  if(kisaWidget->lineEdit->text().isEmpty()) {
    actionAdd->setEnabled(false);
    actionIgnore->setEnabled(false);
  }

  // direction change option
  menu->addSeparator();

  if(QFile("icons:keyboard_layout.png").exists())
    icon = QIcon("icons:keyboard_layout.png");
  else
    icon = QIcon("icons:preferences-desktop-keyboard.png");
  if(kisaWidget->lineEdit->layoutDirection() == Qt::LeftToRight)
    menu->addAction(icon, tr("Set right-to-left layout"), kisaWidget, SLOT(toggleLayoutDirection()));
  else
    menu->addAction(icon, tr("Set left-to-right layout"), kisaWidget, SLOT(toggleLayoutDirection()));

  // the custom command
  QString customCommandName = kisaSettings->customCommandName();
  QIcon customCommandIcon = kisaSettings->customCommandIcon();

  if(customCommandName.isEmpty()) {
    customCommandName = tr("Run Custom Command");
    if(QFile("icons:launch.png").exists())
      customCommandIcon = QIcon("icons:launch.png");
    else
      customCommandIcon = QIcon("icons:system-run.png");
  }

  menu->addSeparator();
  menu->addAction(customCommandIcon, customCommandName, this, SLOT(runCustomCommand()));

  // Use popup() to execute asynchronously or exec() synchronously
  menu->exec(kisaWidget->mapToGlobal(point));

  delete menu;
}

bool Kisa::dictionarySeenBefore(const int& index) {
  foreach(QAction* action, kisaToolButtonMenu->actions())
      if(index == action->data())
        return true;

  return false;
}

bool Kisa::eventFilter(QObject* object, QEvent* event) {

#ifdef FULL_DEBUG
  qDebug() << OBJECT_DEBUG_NAME << "got event type:" << event->type() << "from object:" << object->objectName() << ws << "(" << object->metaObject()->className() << ")";
#endif

  // keyboard layout change
  if(object == kisaWidget && event->type() == QEvent::KeyboardLayoutChange) {
    kisaWidget->lineEdit->clear();

    // temporary disable the event filter
    kisaWidget->removeEventFilter(this);

    if(kisaSettings->updateOnNewLayout()) {
      updateOnNewKeyboardLayout();
    }

    // remove the event from the event queue
    event->accept();

    // flush the event queue
    QCoreApplication::processEvents();

    // reinstall the event filter
    kisaWidget->installEventFilter(this);

    // event was handled
    return true;
  }

  // left-click on misspelled word executes custom command
  else if(object == kisaWidget && event->type() == QEvent::MouseButtonRelease) {
    if(((QMouseEvent *) event)->button() == Qt::LeftButton
        && kisaWidget->lineEdit->cursor().shape() == Qt::PointingHandCursor)
      runCustomCommand();

    // remove the event from the event queue
    event->accept();

    // event was handled
    return true;
  }

  // destroy the event
  event->ignore();

  // event was ignored
  return false;
}

void Kisa::getHelp() {
  QString kisaManualUrl = DEFAULT_MANUAL_URL;
  QDesktopServices::openUrl(QUrl(kisaManualUrl));
}

void Kisa::ignoreWord() {
  QString word = kisaWidget->lineEdit->text();

  if(!word.isEmpty())
    kisaLib->addToSessionWordList(word);
}

void Kisa::init(const QString& display,
                const QString& language,
                const QString& iconsPath,
                const QString& flagsPath,
                const QString& dictDir,
                const QString& aspellPrefix,
                const QString& extraDicts) {
  // instantiate and load the translator
  loadTranslations(language);

  setApplicationName(PROJECT_NAME);
  setOrganizationName(PROJECT_NAME);
  setObjectName(OBJECT_NAME);

  QIcon kisaIcon = QIcon(":/icons/kisa.png");
  setWindowIcon(kisaIcon);

  initLibrary(display, iconsPath, flagsPath, language, dictDir, aspellPrefix,
              extraDicts);

  // main GUI window
  initWidget(kisaIcon);

  // the rest of the members, such as the worker thread, timer and so on
  initMembers();

  // the system tray icon is dependent on kisaWidget
  initSystemTray(kisaIcon);

  // load any settings file left over from previous sessions
  initSession();

  // make all the necessary connections
  initSignalsAndSlots();
}

void Kisa::initLibrary(const QString& display,
                       const QString& iconsPath,
                       const QString& flagsPath,
                       const QString& dictionary,
                       const QString& dictDir,
                       const QString& aspellPrefix,
                       const QString& extraDicts) {
  // instantiate the library
   kisaLib = KisaLib::instance(display);

   // initialize the library
   kisaLib->initialize(iconsPath, flagsPath, dictionary, dictDir, aspellPrefix,
                       extraDicts);
}

void Kisa::initMembers() {
  // settings window
  kisaSettings = new KisaSettings(kisaWidget);

  // main worker thread that handles all I/O, all GUI is done from here
  kisaThread = new KisaThread(this);

  // the tool button pop-up menu containing the dictionaries used so far
  kisaToolButtonMenu = new QMenu(tr("Kisa Tool Button Menu"), kisaWidget);
  kisaToolButtonMenu->setObjectName("kisaToolButtonMenu");
  kisaWidget->toolButton->setMenu(kisaToolButtonMenu);

  // flag that tracks if the word has been passed to the spell checker
  wordIsChecked = false;

  // used as a pop-up to show possible suggestions
  completer = new QCompleter(kisaWidget->lineEdit);

  // set the timer that clears the input if idle
  kisaTimer = new QTimer(this);
  kisaTimer->setSingleShot(true);

  // the punctuation character that triggered the spell check
  wordBuffer = QString();
}

void Kisa::initSession() {
  // check if to restore a previous session's settings
  // make sure this is among the last things to be done
  if(isSessionRestored())
    restoreSession();

  else {
    kisaSettings->readSettings();
    updateSettings();
  }
}

void Kisa::initSignalsAndSlots() {
  connect(kisaTimer, SIGNAL(timeout()), this, SLOT(clear()));

  // input from any application window (worker thread) to GUI connection
  connect(kisaThread, SIGNAL(gotNewCharacter(const QChar&)), this, SLOT(addCharacter(const QChar&)));

  // application visibility state change
  connect(kisaThread, SIGNAL(visibilityStateChange(const int&)), kisaWidget, SLOT(updateVisibilityState(const int&)));

  // restart the worker thread if the settings are changed
  connect(kisaSettings, SIGNAL(accepted()), this, SLOT(updateSettings()));

  // clicking the toolButton switches to the next dictionary in the pop-up list
  connect(kisaWidget->toolButton, SIGNAL(clicked()), this, SLOT(switchToNextDictionary()));

  // clicking on one of the items in the tool button menu
  connect(kisaToolButtonMenu, SIGNAL(triggered(QAction*)), this, SLOT(toolButtonMenuActivated(QAction*)));

  // show or hide the application whenever someone clicks on the icon
  connect(kisaTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(systemTrayIconActivated(QSystemTrayIcon::ActivationReason)));

  // adds the custom command to the bottom of the context menu of the lineEdit
  // whenever a context menu is requested, that is the custim context menu
  connect(kisaWidget->lineEdit, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(contextMenuEvent(const QPoint&)));
}

void Kisa::initSystemTray(const QIcon& kisaIcon) {
  kisaTrayIcon = new QSystemTrayIcon(kisaIcon, this);
  kisaTrayIcon->setObjectName("kisaTrayIcon");

  // and its menu
  kisaTrayIconMenu = new QMenu(tr("Kisa Tray Icon Menu"), kisaWidget);
  kisaTrayIconMenu->setIcon(kisaIcon);
  kisaTrayIconMenu->setObjectName("kisaTrayIconMenu");
  kisaTrayIcon->setContextMenu(kisaTrayIconMenu);

  updateSystemTrayIconMenu(true);
}

void Kisa::initWidget(const QIcon& kisaIcon) {
  kisaWidget = new KisaWidget();
  kisaWidget->setWindowFlags(Qt::Tool);
  kisaWidget->setWindowIcon(kisaIcon);

  // event filer used for checking if the keyboard input layout has changed
  kisaWidget->installEventFilter(this);

  // event filer used for catching mouse click events
  kisaWidget->lineEdit->installEventFilter(this);
}

bool Kisa::isValidForOutput(const QChar& key) {
  QChar hyphen = '-';
  QChar apostrophe = '\'';

  // check the input buffer for crap
  foreach(QChar ch, wordBuffer) {
    if(ch.isNumber())
      return false;
  }

  if(key.isLetter())
    return true;

  if(key == hyphen && !hyphenAsPunctuation)
    return true;

  if(key == apostrophe )
    return true;

  return false;
}

bool Kisa::isValidForOutput(const QString& word) {
  foreach(QChar ch, word) {
    if(!isValidForOutput(ch)) {
      qDebug() << OBJECT_DEBUG_NAME << "the word " << word << " is not valid for output";
      return false;
    }
  }
  return true;
}

bool Kisa::isValidPunctuation(const QChar& key) {
  if(key.isSpace())
    return true;

  if(key == '-' && hyphenAsPunctuation)
    return true;

  if(key.isPunct() && key != '-' && key != '\'')
    return true;

  return false;
}

void Kisa::loadTranslations(QString locale) {
  // load the translator according the current system locale if none is given
  if(locale.isEmpty())
    locale = QLocale::system().name();

  // add the application icon path as well
  QString path = QCoreApplication::applicationDirPath();
  // remove the "bin" folder at the end and add the application icons path
  path.chop(3);
  path += "share/kisa/translations/";

  QString translation = "kisa_" + locale;

  if(kisaTranslator->load(translation, path)) {
    installTranslator(kisaTranslator);
    qDebug() << OBJECT_DEBUG_NAME << "using translation given by language locale" << locale;
  }
  // ignore default (American) locale
  else if(locale != "en_US" || locale != "C") {
    qDebug() << OBJECT_DEBUG_NAME << "no translation found for locale" << locale << "using default application language";
  }
}

void Kisa::parseInputArguments(int& argc,
                               char** argv,
                               QString& display,
                               QString& language,
                               QString& iconsPath,
                               QString& flagsPath,
                               QString& dictDir,
                               QString& aspellPrefix,
                               QString& extraDicts) {
  // parse the input argument's values
  // as they are on the form "-option value" we always want the next string in
  // in the array (++i)
  // there are more arguments available then these underneath, so we need not
  // worry about an arrayOutOfBounds, try a arg[15] for instance...
  for(int i = 1; i < argc; i++) {
    QString argument = QString::fromUtf8(argv[i]);

    if(argument == tr("-help"))
      showCommandLineHelp();

    else if(argument == tr("-display"))
      display = QString::fromUtf8(argv[++i]);

    else if(argument == tr("-language"))
      language = QString::fromUtf8(argv[++i]);

    else if(argument == tr("-icons"))
      iconsPath = QString::fromUtf8(argv[++i]);

    else if(argument == tr("-flags"))
      flagsPath = QString::fromUtf8(argv[++i]);

    else if(argument == tr("-dict-dir"))
      dictDir = QString::fromUtf8(argv[++i]);

    else if(argument == tr("-aspell-prefix"))
      aspellPrefix = QString::fromUtf8(argv[++i]);

    else if(argument == tr("-extra-dicts"))
      extraDicts = QString::fromUtf8(argv[++i]).simplified();

    else if(argument == tr("-extra-dicts"))
      extraDicts = QString::fromUtf8(argv[++i]).simplified();

    else {
      QString message = tr("Unknown option") + " " + QString::fromUtf8(argv[i]);
      message += "\n";
      message
          += tr("Use -help to get a list of available command line options");
      qFatal("%s", message.toLocal8Bit().constData());
    }
  }
}

void Kisa::resetToolButtonMenu() {
  kisaToolButtonMenu->clear();

  kisaToolButtonMenu->addSeparator();
  kisaToolButtonMenu->addAction(QIcon("icons:edit-clear-history.png"),
                            tr("Clear used dictionaries"),
                            this,
                            SLOT(resetToolButtonMenu()));

  kisaWidget->toolButton->setStyleSheet("");

  updateToolButtonMenu();

  qDebug() << OBJECT_DEBUG_NAME << "tool button menu has been cleared and updated";
}

void Kisa::restartThread() {
  if(kisaThread->isRunning()) {
    // call the thread method instead of toggleThread as we don't need to
    // update the icons
    kisaThread->startToggle();
    kisaThread->startToggle();

    qDebug() << OBJECT_DEBUG_NAME << "restarting thread...";
  }
}

void Kisa::restoreSession() {
  const QString sessionID = sessionId();

  // get the default settings file name
  QString fileName = QSettings().fileName().toLower();

  // add the session id
  fileName.replace(QRegExp("\\.conf$"), "_" + sessionID + ".conf");

  // stop if the file doesn't exist
  if(!QFile::exists(fileName)) {
    qDebug() << OBJECT_DEBUG_NAME << "could not find session file, using previous settings";

    kisaSettings->readSettings();
    updateSettings();

    return;
  }

  qDebug() << OBJECT_DEBUG_NAME << "restoring session from file" << fileName;

  // get the dictionaries
  QSettings settings(fileName, QSettings::IniFormat, this);
  settings.beginGroup("Session");
  QStringList dictionaryIndexes =
      settings.value("dictionaryIndexes").toStringList();
  settings.endGroup();

  // if there is only 1 dictionary or less no need to add it to the list
  if(dictionaryIndexes.size() > 1) {
    // clear everything so we get the previous order intact
    kisaToolButtonMenu->clear();
    kisaToolButtonMenu->addSeparator();
    kisaToolButtonMenu->addAction(QIcon("icons:history_clear.png"),
                              tr("Clear used dictionaries"), this, SLOT(resetToolButtonMenu()));

    foreach(QString dictionaryIndex, dictionaryIndexes) {
      int index = dictionaryIndex.toInt();
      QIcon icon = kisaSettings->countryFlag(index);
      addDictionaryToMenu(index, icon);
    }
  }

  // the rest of the settings
  kisaSettings->readSettings(fileName);

  // this also calls updateToolButtonMenu
  updateSettings();

  // don't need the file any more
  QFile::remove(fileName);
}

void Kisa::runCustomCommand() {
  QString customCommand = kisaSettings->customCommand();

  // substitute language and word if user specified any
  customCommand.replace("$LANG", kisaLib->currentDictionaryCode().left(2));
  customCommand.replace("$WORD", kisaWidget->lineEdit->text());

  // check if the command is a web url and use the system default method of
  // opening it, else execute the command
  QUrl url(customCommand);
  if(!url.isRelative()) {
    if(!QDesktopServices::openUrl(customCommand)) {
      qDebug() << OBJECT_DEBUG_NAME << "unable to open url" << customCommand << "using current system settings";
    }
  }
  else {
    // block and wait for process to start
    if(!QProcess::startDetached(customCommand)) {
      qDebug() << OBJECT_DEBUG_NAME << "unable to execute custom command" << customCommand;
    }
  }
}

void Kisa::showAbout() {
  // some fancy html for the author email
  QString message = "<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">, li { white-space: pre-wrap; }</style></head><body style=\" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;\">Kisa &#8212; live! spell checker ";
  message += PROJECT_VERSION;
  message += "<br><br>(c) 2009 <a href=\"mailto:theblackpeter@gmail.com?subject=Something%20about%20the%20great%20application%20Kisa\">Pete Black</a> (theblackpeter@gmail.com)</body></html>";

  QMessageBox::about(kisaWidget, tr("About Kisa"), message);
}

void Kisa::showCommandLineHelp() {
  QTextStream out(stdout);
  out << QString(PROJECT_NAME) << " " << QString(PROJECT_VERSION)
      << " Copyright (c) 2009 by Pete Black" << endl;
  out << QString(PROJECT_NAME) << " " << tr("-- live! spell checker") << endl;
  out << endl;
  out << tr("Usage: ") << OBJECT_NAME << " " << tr("[options]") << endl;
  out << endl;
  out << tr("Options:") << endl;
  out << "\t" << tr("-help") << "\t" << tr("Shows this help") << endl;
  out << "\t" << tr("-display <display>") << "\n\t\t"
      << tr("The display on the X server to use") << endl;
  out << "\t" << tr("-language <language>") << "\n\t\t"
      << tr("The application language to use") << endl;
  out << "\t" << tr("-icons <path>") << "\n\t\t"
      << tr("Where to look for action icons") << endl;
  out << "\t" << tr("-flags <path>") << "\n\t\t"
      << tr("Where to look for country flag icons") << endl;
  out << "\t" << tr("-dict-dir <path>") << "\n\t\t"
      << tr("Main word list location") << endl;
  out << "\t" << tr("-aspell-prefix <path>") << "\n\t\t"
      << tr("Aspell prefix directory") << endl;
  out << "\t" << tr("-extra-dicts <list>") << "\n\t\t"
      << tr("List of extra dictionaries to use") << endl;
  out << endl;
  out << tr("Example:") << endl;
  out << "\t" << QString(OBJECT_NAME) << " "
      << tr("-language sv -icons ~/myTheme -flags /usr/share/locale/l10n")
      << endl;
  out << endl;
  out << tr("Will use Swedish as application language, the icons from the theme located under\n\"~/myTheme\" and the flag icons from \"/usr/share/locale/l10n\"")
      << endl;

  // TODO: move this out to main.cpp and pass a shouldStart bool flag
  std::exit(0);
}

void Kisa::startThread(const bool updateIconMenu) {
  if(!kisaThread->isRunning())
    toggleThread(updateIconMenu);
}

void Kisa::stopThread(const bool updateIconMenu) {
  if(kisaThread->isRunning())
    toggleThread(updateIconMenu);
}

void Kisa::switchToNextDictionary() {
  QList<QAction*> actions = kisaToolButtonMenu->actions();

  // ignore the separator and the "clear" action that where added at the end
  int actionsSize = actions.size() - 2;

  // don't bother if there is only one dictionary
  if(actionsSize <= 1)
    return;

  int dictionaryIndex = kisaLib->currentDictionaryIndex();

  // yes a regular for loop, need the index incremented here...
  for(int i = 0; i < actionsSize; i++) {
    if(dictionaryIndex == actions.at(i)->data()) {
      // we want the subsequent element, but if we are at the end then the next
      // is the first... I know, it's getting late here...
      if(i == actionsSize - 1)
        i = 0;
      else
        i++;

      // use the toolButtonMenuActivated slot to update both the flag and the
      // dictionary
      toolButtonMenuActivated(actions.at(i));

      return;
    }
  }

  qDebug() << OBJECT_DEBUG_NAME << "sorry, there is no new dictionary to switch to, try adding some to the list";
}

void Kisa::systemTrayIconActivated(QSystemTrayIcon::ActivationReason reason) {
  if(reason == QSystemTrayIcon::Trigger) {
    kisaWidget->lineEdit->clear();
    wordBuffer.clear();

    // show/hide the main widget
    if(kisaWidget->isVisible()) {
      kisaWidget->hide();
      kisaTrayIcon->setIcon(QIcon(":/icons/kisa_shaded.png"));
      stopThread();
    }
    else {
      kisaWidget->showNormal();
      kisaTrayIcon->setIcon(QIcon(":/icons/kisa.png"));
      startThread();
    }
  }
}

void Kisa::toggleThread(const bool updateIconMenu) {
  kisaThread->startToggle();

  if(updateIconMenu)
    updateSystemTrayIconMenu();
}

void Kisa::toolButtonMenuActivated(QAction* action) {
  int index = action->data().toInt();
  updateDictionary(index);

  // no need to update the entire pop-up menu, just the icon
  updateToolButtonIcon();
}

void Kisa::updateDictionary(const int& index) {
  // try to update both the dictionary in the library and the one in the
  // settings menu based on index
  if(kisaLib->updateDictionary(index)) {
    kisaSettings->setDictionaryIndex(index);
    QString dictionary = kisaLib->currentDictionary();
    QString message = tr("Kisa -- using dictionary %1").arg(dictionary);
    kisaTrayIcon->setToolTip(message);
    kisaWidget->setToolTip(message);
    kisaWidget->toolButton->setToolTip(dictionary);

    // update the text input direction
    updateTextDirection();
  }
}

void Kisa::updateDictionary(const QString& layout) {
  // try to update both the dictionary in the library and the one in the
  // settings menu based on a 5 char language_COUNTRY layout
  if(kisaLib->updateDictionary(layout)) {
    int dictionaryIndex = kisaLib->currentDictionaryIndex();
    kisaSettings->setDictionaryIndex(dictionaryIndex);

    QString dictionary = kisaLib->currentDictionary();
    QString message = tr("Kisa -- using dictionary %1").arg(dictionary);
    kisaTrayIcon->setToolTip(message);
    kisaWidget->setToolTip(message);
    kisaWidget->toolButton->setToolTip(dictionary);

    // update the text input direction
    updateTextDirection();
  }
}

void Kisa::updateOnNewKeyboardLayout() {
  // get the new layout code
  QString layout = kisaLib->getKeyboardInputLayout();
  QString dictionaryCode = kisaLib->currentDictionaryCode();
  int codeSize = dictionaryCode.length();

  if(layout.left(codeSize) != dictionaryCode) {
    // update the dictionary accordingly to the new language
    updateDictionary(layout);

    // update the tool button's pop-up menu
    updateToolButtonMenu();
  }
  else {
    qDebug() << OBJECT_DEBUG_NAME <<  "keyboard layout same as current dictionary, no update made";
  }
}

void Kisa::updateSettings() {
  // update the dictionary, always do this first
  int currentDictionaryIndex = kisaLib->currentDictionaryIndex();
  int dictionaryIndex = kisaSettings->dictionaryIndex();
  if(currentDictionaryIndex != dictionaryIndex)
    updateDictionary(dictionaryIndex);

  bool visible = kisaWidget->isVisible();

  // stay on top or not
  if(kisaSettings->stayOnTop()) {
    // only make changes if needed as this hides the window for some reason
    if(!(kisaWidget->windowFlags() & Qt::WindowStaysOnTopHint)) {
      kisaWidget->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);

      // NOTE: when changing the window flags the window gets minimized, or the
      // application can even shut down for some reason
      if(visible)
        kisaWidget->show();
    }
  }
  else {
    // only make changes if needed as this hides the window for some reason
    if(kisaWidget->windowFlags() & Qt::WindowStaysOnTopHint) {
      kisaWidget->setWindowFlags(Qt::Tool);

      // NOTE: when changing the window flags the window gets minimized, or the
      // application can even shut down for some reason
      if(visible)
        kisaWidget->show();
    }
  }

  // update the font
  kisaWidget->lineEdit->setFont(kisaSettings->font());

  // show tool button or not?
  if(kisaSettings->showButton()) {
    updateToolButtonMenu();
    updateToolButtonIconHistory();

    kisaWidget->toolButton->show();
  }
  else
    kisaWidget->toolButton->hide();

  // convenience variable to reduce function calls
  hyphenAsPunctuation = kisaSettings->hyphenAsPunctuation();

  // the window ignore list
  QString ignoreList = kisaSettings->ignoreListRegExp();
  kisaLib->setIgnoreList(ignoreList);
  qDebug() << OBJECT_DEBUG_NAME << "setting the ignore list to" << ignoreList;

  qDebug() << OBJECT_DEBUG_NAME << "settings updated";

  if(kisaWidget->isVisible())
    restartThread();
}

void Kisa::updateSystemTrayIconMenu(bool active) {
  // clear the menu completely and rebuild it, not very efficient, but much
  // easier then trying to replace items at certain locations
  kisaTrayIconMenu->clear();

  // if the thread is running we want the stop action at the top
  if(active || kisaThread->isRunning()) {
    kisaWidget->lineEdit->setEnabled(true);
    kisaWidget->toolButton->setEnabled(true);

    kisaTrayIconMenu->addAction(QIcon("icons:process-stop.png"), tr("&Disable"),
                            this, SLOT(stopThread()));

    // update the tooltips
    QString dictionary = kisaLib->currentDictionary();
    QString message = tr("Kisa -- using dictionary %1").arg(dictionary);
    kisaTrayIcon->setToolTip(message);
    kisaWidget->setToolTip(message);
    kisaWidget->toolButton->setToolTip(dictionary);

    // update the icon
    QIcon kisaIcon = QIcon(":/icons/kisa.png");
    setWindowIcon(kisaIcon);
    kisaTrayIcon->setIcon(kisaIcon);
    kisaWidget->setWindowIcon(kisaIcon);
  }
  // if not then make the run action available instead
  else {
    kisaWidget->lineEdit->setEnabled(false);
    kisaWidget->toolButton->setEnabled(false);

    kisaTrayIconMenu->addAction(QIcon("icons:system-run.png"), tr("&Enable"), this,
                            SLOT(startThread()));

    // update the tooltips
    QString message = tr("Kisa is not running");
    kisaTrayIcon->setToolTip(message);
    kisaWidget->setToolTip(message);
    kisaWidget->toolButton->setToolTip(message);

    // update the icon
    QIcon kisaIcon = QIcon(":/icons/kisa_shaded.png");
    setWindowIcon(kisaIcon);
    kisaTrayIcon->setIcon(kisaIcon);
    kisaWidget->setWindowIcon(kisaIcon);
  }

  kisaTrayIconMenu->addSeparator();
  kisaTrayIconMenu->addAction(QIcon("icons:kwin.png"), tr("R&estore"), kisaWidget,
                          SLOT(showNormal()));
  kisaTrayIconMenu->addAction(QIcon("icons:configure.png"), tr("&Settings"),
                          kisaSettings, SLOT(showNormal()));
  kisaTrayIconMenu->addSeparator();

  if(QFile("icons:help-about.png").exists())
    kisaTrayIconMenu->addAction(QIcon("icons:help-about.png"), tr("&Help"), this,
                            SLOT(getHelp()));
  else
    kisaTrayIconMenu->addAction(QIcon("icons:help.png"), tr("&Help"), this, SLOT(getHelp()));

  kisaTrayIconMenu->addSeparator();
  kisaTrayIconMenu->addAction(QIcon(":/icons/kisa.png"), tr("&About Kisa"), this,
                          SLOT(showAbout()));
  kisaTrayIconMenu->addAction(QIcon(":/icons/qt-logo.png"), tr("A&bout Qt"), this,
                          SLOT(aboutQt()));
  kisaTrayIconMenu->addSeparator();
  kisaTrayIconMenu->addAction(QIcon("icons:application-exit.png"), tr("&Quit"),
                          this, SLOT(quit()));
}

void Kisa::updateText(const QString& newWord) {
  qDebug() << OBJECT_DEBUG_NAME << "selected" << newWord << "from pop-up";

  // save the current text before changing it
  QString word = kisaWidget->lineEdit->text();

  // add to system clipboard
  if(kisaSettings->addToClipboard()) {
    QClipboard *clipboard = QApplication::clipboard();
    clipboard->setText(newWord);
    qDebug() << OBJECT_DEBUG_NAME << "selected word" << newWord << "added to the clipboard";
  }

  // replace misspelled word in client application with the selected
  // as it is the worker thread that does all the interaction with the kisa
  // events, and remembers where they came from, it's the only one that can do
  // this (except for a call to the library directly)
  if(kisaSettings->replaceInClient()) {
    // disable all event notifications without making updates to the system tray
    stopThread(false);

    // get the residual, that is what's left after the word
    QString residual = wordBuffer.remove(word, Qt::CaseSensitive);
    // send the word back to the client
    kisaThread->replaceInClient(word, newWord, residual);

    // enable event notifications again
    startThread(false);
  }

  kisaWidget->lineEdit->setStyleSheet("");
  kisaWidget->lineEdit->setFont(kisaSettings->font());

  // change the mouse pointer to default
  kisaWidget->lineEdit->setCursor(Qt::ArrowCursor);

  kisaWidget->lineEdit->setText(newWord);
}

void Kisa::updateTextDirection() {
  kisaWidget->lineEdit->setLayoutDirection(keyboardInputDirection());
  kisaWidget->lineEdit->setLocale(keyboardInputLocale());
}

QIcon Kisa::updateToolButtonIcon() {
  // get the icon of the current dictionary
  QIcon icon;

  if(kisaSettings->showFlag())
    icon = kisaSettings->currentCountryFlag();

  // if we don't want an icon or if none is found show text
  if(icon.isNull()) {
    // show the button with the country as text
    kisaWidget->toolButton->setAutoRaise(false);
    kisaWidget->toolButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
    QString countryCode = kisaLib->currentDictionaryCode().left(2);
    kisaWidget->toolButton->setText(countryCode);

    qDebug() << OBJECT_DEBUG_NAME << "no icon for the selected dictionary found, using text instead";
  }
  else {
    kisaWidget->toolButton->setAutoRaise(true);
    kisaWidget->toolButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
    kisaWidget->toolButton->setIcon(icon);

    // make sure the icon is no larger then 21x14 (which most flag icons in
    // /usr/share/locale/l10n/ are)
    kisaWidget->toolButton->setIconSize(icon.actualSize(QSize(21, 14)));

    qDebug() << OBJECT_DEBUG_NAME << "updated tool button icon";
  }

  return icon;
}

void Kisa::updateToolButtonIconHistory() {
  QStringList dictionaryIndexes;
  foreach(QAction* action, kisaToolButtonMenu->actions())
      if(!action->data().isNull())
        dictionaryIndexes.prepend(action->data().toString());

  resetToolButtonMenu();

  bool showFlag = kisaSettings->showFlag();

  foreach(QString dictionaryIndex, dictionaryIndexes) {
    int index = dictionaryIndex.toInt();

    QIcon icon;
    if(showFlag)
      icon = kisaSettings->countryFlag(index);

    addDictionaryToMenu(index, icon);
  }
}

// make sure the dictionary has been updated first
void Kisa::updateToolButtonMenu() {
  // first time eh?, create a new menu then
  if(kisaToolButtonMenu->isEmpty()) {
    resetToolButtonMenu();

    // resetToolButtonMenu() will take care of the rest
    return;
  }

  // get the current dictionary and index
  QString dictionary = kisaLib->currentDictionary();
  int dictionaryIndex = kisaLib->currentDictionaryIndex();

  // update the toolButton icon, we don't need the returned icon here
  updateToolButtonIcon();

  // get the icon, if any is used
  QIcon icon = kisaSettings->currentCountryFlag();

  // add the dictionary to the popup
  addDictionaryToMenu(dictionaryIndex, icon);
}
